# Copyright 2013 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Client configuration management.

This module holds the code for detecting and configuring the current client and
it's output directories.
It is responsible for writing out the client specific plugins that tell the
rest of the cr tool what the client is capable of.
"""

from __future__ import print_function

import os
import pprint
import sys

import cr
import cr.auto.build
import cr.auto.client

# The config version currently supported.
VERSION = 0.5
# The default directory name to store configs inside.
CONFIG_PATH = '.cr'
# The filename of the config file inside a config directory.
CONFIG_FILE = 'config.py'
# The directory inside the config directory which contains the client config.
CLIENT_CONFIG_DIR = 'client'
# The directory inside the config directory which contains build configs.
BUILD_CONFIG_DIR = 'builds'
# The format string for the header of a config file.
CONFIG_FILE_PREFIX = """
# This is an autogenerated file
# it *will* be overwritten, and changes may lost
# The system will autoload any other python file in the same folder.

import cr

OVERRIDES = cr.Config.From("""
# The format string for each value in a config file.
CONFIG_VAR_LINE = '\n  {0} = {1!r},'
# The format string for the tail of a config file.
CONFIG_FILE_SUFFIX = '\n)\n'
# The name of the gclient config file
GCLIENT_FILENAME = '.gclient'

# The default config values installed by this module.
DEFAULT = cr.Config.From(
    CR_ROOT_PATH=os.path.join('{GOOGLE_CODE}'),
    CR_CLIENT_NAME='chromium',
    CR_CLIENT_PATH=os.path.join('{CR_ROOT_PATH}', '{CR_CLIENT_NAME}'),
    CR_SRC=os.path.join('{CR_CLIENT_PATH}', 'src'),
    CR_BUILD_DIR=os.path.join('{CR_SRC}', '{CR_OUT_FULL}'),
)


def DetectClient():
  # Attempt to detect the current client from the cwd
  # See if we can detect the source tree root
  client_path = os.getcwd()
  while (client_path and
         not os.path.exists(os.path.join(client_path, GCLIENT_FILENAME))):
    old = client_path
    client_path = os.path.dirname(client_path)
    if client_path == old:
      client_path = None
  if client_path is not None:
    dirname, basename = os.path.split(client_path)
    if basename == 'src':
      # we have the src path, base is one level up
      client_path = dirname
  if client_path is not None:
    cr.context.derived['CR_CLIENT_PATH'] = client_path
  # now get the value from it may be different
  client_path = cr.context.Get('CR_CLIENT_PATH')
  if client_path is not None:
    cr.context.derived['CR_CLIENT_NAME'] = os.path.basename(client_path)


def _GetConfigDir(use_build_dir):
  base_path = os.path.join(cr.context.Get('CR_CLIENT_PATH'), CONFIG_PATH)
  if use_build_dir:
    path_suffix = os.path.join(BUILD_CONFIG_DIR, cr.context.Get('CR_OUT_FULL'))
  else:
    path_suffix = CLIENT_CONFIG_DIR
  return os.path.realpath(os.path.join(base_path, path_suffix))


def _GetDeprecatedConfigDir(use_build_dir):
  if use_build_dir:
    path = cr.context.Get('CR_BUILD_DIR')
  else:
    path = cr.context.Get('CR_CLIENT_PATH')
  return os.path.realpath(os.path.join(path, CONFIG_PATH))


def _GetConfigFile(config_dir):
  return os.path.join(config_dir, CONFIG_FILE)


def _MigrateAndGetConfigDir(use_build_dir):
  new_config_dir = _GetConfigDir(use_build_dir)
  new_config_file = _GetConfigFile(new_config_dir)
  new_config_exists = os.path.exists(new_config_file)

  old_config_dir = _GetDeprecatedConfigDir(use_build_dir)
  old_config_file = _GetConfigFile(old_config_dir)
  old_config_exists = os.path.exists(old_config_file)

  if old_config_exists:
    if new_config_exists:
      print('Warning: Old config file %s superseded by new config file %s' %
            (old_config_file, new_config_file))
    else:
      print('Migrating config file from %s to %s...' % (old_config_file,
                                                        new_config_file))
      if not cr.context.dry_run:
        # Make the new config directory (if necessary).
        try:
          os.makedirs(new_config_dir)
        except OSError:
          if not os.path.isdir(new_config_dir):
            raise
        # Move the config file.
        os.rename(old_config_file, new_config_file)
        # Delete the old config directory (only applies to the build config).
        if use_build_dir:
          try:
            os.removedirs(old_config_dir)
          except OSError:
            print('Warning: Old config directory %s could not be removed' %
                  (old_config_dir))

  return new_config_dir


def _WriteConfig(writer, data):
  writer.write(CONFIG_FILE_PREFIX)
  for key, value in data.items():
    writer.write(CONFIG_VAR_LINE.format(key, value))
  writer.write(CONFIG_FILE_SUFFIX)


def AddArguments(parser):
  parser.add_argument(
      '-o', '--out', dest='_out', metavar='name',
      default=None,
      help='The name of the out directory to use. Overrides CR_OUT.'
  )


def GetOutArgument():
  return getattr(cr.context.args, '_out', None)


def ApplyOutArgument():
  # TODO(iancottrell): be flexible, allow out to do approximate match...
  out = GetOutArgument()
  if out:
    cr.context.derived.Set(CR_OUT_FULL=out)


def ReadGClient():
  """Loads the .gclient configuration for the current client.

  This will load from CR_CLIENT_PATH.

  Returns:
    The dict of values set in the .gclient file.

  """
  # Now attempt to load and parse the .gclient file
  result = {}
  try:
    gclient_file = cr.context.Substitute(
        os.path.join('{CR_CLIENT_PATH}', GCLIENT_FILENAME))
    with open(gclient_file, 'r') as spec_file:
      # matching the behaviour of gclient, so pylint: disable=exec-used
      exec(spec_file.read(), {}, result)
  except IOError:
    # no .gclient file, skip it
    pass
  return result


def WriteGClient():
  """Writes the .gclient configuration for the current client.

  This will write to CR_CLIENT_PATH.

  """
  gclient_file = cr.context.Substitute(
      os.path.join('{CR_CLIENT_PATH}', GCLIENT_FILENAME))
  spec = '\n'.join('%s = %s' % (key, pprint.pformat(value))
      for key,value in cr.context.gclient.items())
  if cr.context.dry_run:
    print('Write the following spec to', gclient_file)
    print(spec)
  else:
    with open(gclient_file, 'w') as spec_file:
      spec_file.write(spec)

def LoadConfig():
  """Loads the client configuration for the given context.

  This will load configuration if present from CR_CLIENT_PATH and then
  CR_BUILD_DIR.

  Returns:
    True if configuration was fully loaded.

  """
  # Load the root config, will help set default build dir
  client_config_dir = _MigrateAndGetConfigDir(use_build_dir=False)
  cr.auto.client.__path__.append(client_config_dir)
  cr.loader.Scan()
  # Now load build dir config
  build_config_dir = _MigrateAndGetConfigDir(use_build_dir=True)
  cr.auto.build.__path__.append(build_config_dir)
  cr.loader.Scan()

  if not hasattr(cr.auto.build, 'config'):
    return False

  cr.context.derived.Set(CR_BUILD_CONFIG_PATH=_GetConfigFile(build_config_dir))
  return True


def WriteConfig(use_build_dir, data):
  """Writes a configuration out to a file.

  This writes all the key value pairs in data out to a config file.

  Args:
    use_build_dir: True if the config file should be written to the build
        directory. Otherwise it will be written to the root config directory.
    data: The key value pairs to write.
  """
  config_dir = _GetConfigDir(use_build_dir)
  filename = _GetConfigFile(config_dir)
  if cr.context.dry_run:
    print('makedirs', config_dir)
    print('Write config to', filename)
    _WriteConfig(sys.stdout, data)
  else:
    try:
      os.makedirs(config_dir)
    except OSError:
      if not os.path.isdir(config_dir):
        raise
    with open(filename, 'w') as writer:
      _WriteConfig(writer, data)


def PrintInfo():
  print('Selected output directory is', cr.context.Find('CR_BUILD_DIR'))
  print('Build config file is', _GetConfigFile(
      _GetConfigDir(use_build_dir=True)))
  try:
    for name in cr.auto.build.config.OVERRIDES.exported.keys():
      print(' ', name, '=', cr.context.Get(name))
  except AttributeError:
    pass


class InitHook(cr.Plugin, cr.Plugin.Type):
  """Base class for output directory initialization hooks.

  Implementations used to fix from old version to new ones live in the
  cr.fixups package.
  """

  def Run(self, old_version, config):
    """Run the initialization hook.

    This is invoked once per init invocation.
    Args:
      old_version: The old version,
          0.0 if the old version was bad or missing,
          None if building a new output direcory.
      config: The mutable config that will be written.
    """
    raise NotImplementedError('Must be overridden.')
